#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include <math.h>
#include <string.h>

#include "bcd.h"

struct _bcd_t {
    uint8_t l: 4;
    uint8_t h: 4;
}__attribute__((packed));

struct _sig_bcd_t {
    uint8_t l: 4;
    uint8_t h: 3;
    _Bool sig: 1;
}__attribute__((packed));

static uint32_t _pow10_tbl_8[8] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000};

static uint64_t _pow10_tbl_12[12] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000,
                                     100000000, 1000000000, 10000000000, 100000000000};

static uint32_t _pow10(uint8_t e) {
    uint8_t tbl_size = sizeof(_pow10_tbl_8) / sizeof(_pow10_tbl_8[0]);

    if (e < tbl_size) {
        return _pow10_tbl_8[e];
    } else {
        int i;
        uint64_t result = 10000000;

        for (i = tbl_size; i < e; ++i) {
            result *= 10;
        }

        return result;
    }
}

uint8_t get_bcd(byte b) {
    return (b >> 4) * 10 + (b & 0x0f);
}

void set_bcd(byte *b, uint8_t value) {
    struct _bcd_t *bcd = (struct _bcd_t *) b;

    bcd->l = value % 10;
    bcd->h = (value / 10) % 10;
}

const char *sprintf_bcd(char *buf, const byte *b, uint8_t digits) {
    unsigned int i, j;

    for (i = (digits + 1) / 2 - 1, j = 0; i >= 0 && j < digits; --i, j += 2) {
        sprintf(buf + j, "%02x", b[i]);
    }

    return buf;
}

void sscanf_bcd(const char *buf, byte *b, uint8_t digits) {
    unsigned int i, j;
    int value = 0;

    for (i = (digits + 1) / 2 - 1, j = 0; i >= 0 && j < digits; --i, j += 2) {
        if (sscanf(buf + j, "%02x", &value) >= 1) {
            b[i] = value;
        } else {
            b[i] = 0;
        }
    }
}

uint32_t read_unsigned_int(const byte *b, uint8_t digits) {
    int i;
    uint32_t result = 0;

    for (i = 0; i < digits / 2; ++i) {
        result += get_bcd(b[i]) * _pow10_tbl_8[i * 2];
    }

    return result;
}

void write_unsigned_int(byte *b, uint8_t digits, uint32_t value) {
    int i;

    for (i = 0; i < digits / 2; ++i) {
        set_bcd(&b[i], value);
        value /= 100;
    }
}

int32_t read_int(const byte *b, uint8_t digits) {
    _Bool negative;
    int32_t result;
    struct _sig_bcd_t *sig;

    sig = (struct _sig_bcd_t *) &b[(digits - 1) / 2];
    negative = sig->sig;
    sig->sig = 0;
    result = read_unsigned_int(b, digits);

    return negative ? -result : result;
}

void write_int(byte *b, uint8_t digits, int32_t value) {
    _Bool negative;
    struct _sig_bcd_t *sig;

    negative = value < 0;
    value = value < 0 ? -value : value;

    write_unsigned_int(b, digits, value);

    sig = (struct _sig_bcd_t *) &b[(digits - 1) / 2];
    sig->sig = negative;
}

double read_decimal(const byte *b, uint8_t digits, uint8_t decimals) {
    double result = read_int(b, digits);

    return result / _pow10(decimals);
}

double read_unsigned_decimal(const byte *b, uint8_t digits, uint8_t decimals) {
    double result = read_unsigned_int(b, digits);

    return result / _pow10(decimals);
}

uint64_t read_unsigned_int64(const byte *b, uint8_t digits) {
    uint8_t i;
    uint64_t result = 0;

    for (i = 0; i < digits / 2; ++i) {
        result += get_bcd(b[i]) * _pow10_tbl_12[i * 2];
    }

    return result;
}

void write_unsigned_int64(byte *b, uint8_t digits, uint64_t value) {
    unsigned int i;

    for (i = 0; i < digits / 2; ++i) {
        set_bcd(&b[i], value);
        value /= 100;
    }
}

time_t read_datetime(const byte *b, uint8_t digits, date_offset_e offset) {
    unsigned int i, j;
    time_t sys_time;
    struct tm *tm;

    time(&sys_time);
    tm = localtime(&sys_time);

    switch (offset) {
        case DT_OFFSET_YEAR:
            tm->tm_year = 2000;
            /* no break */
        case DT_OFFSET_MONTH:
            tm->tm_mon = 1;
            /* no break */
        case DT_OFFSET_DAY:
            tm->tm_mday = 1;
            /* no break */
        case DT_OFFSET_HOUR:
            tm->tm_hour = 0;
            /* no break */
        case DT_OFFSET_MINUTE:
            tm->tm_min = 0;
            /* no break */
        case DT_OFFSET_SECOND:
            tm->tm_sec = 0;
            break;
        default:
            break;
    }

    for (i = 0, j = offset + 2 - digits; i < digits / 2; ++i, j += 2) {
        int value = get_bcd(b[i]);
        switch (j) {
            case DT_OFFSET_SECOND:
                tm->tm_sec = value;
                break;
            case DT_OFFSET_MINUTE:
                tm->tm_min = value;
                break;
            case DT_OFFSET_HOUR:
                tm->tm_hour = value;
                break;
            case DT_OFFSET_DAY:
                tm->tm_mday = value;
                break;
            case DT_OFFSET_MONTH:
                tm->tm_mon = value - 1;
                break;
            case DT_OFFSET_YEAR:
                tm->tm_year = (value > 70) ? 0 : 100 + value;
                break;
            default:
                break;
        }
    }

    return mktime(tm);
}
